#include "LuaTable.h"
#include "LuaFile.h"
#include <lua.hpp>
#include <algorithm>
#include <assert.h>

using std::string;
using std::map;
using std::vector;

/***************************************************************************/

LuaTable::LuaTable() :
	L(NULL),
	m_RefNum(LUA_NOREF),
	m_LuaFile(NULL),
	m_bIsValid(false),
	m_szPath("")
{
}


LuaTable::LuaTable(LuaFile* file)
{
	assert(file != NULL);

	m_bIsValid	= file->IsValid();
	m_szPath	= "ROOT";
	m_LuaFile	= file;
	L			= file->L;
	m_RefNum	= file->m_RootRef;

	if (PushTable())
	{
		lua_pushvalue(L, -1); // Copy
		m_RefNum = luaL_ref(L, LUA_REGISTRYINDEX);
	}
	else
		m_RefNum = LUA_NOREF;

	m_bIsValid = (m_RefNum != LUA_NOREF);

	m_LuaFile->AddTable(this);
}


LuaTable::LuaTable(const LuaTable& table)
{
	m_LuaFile	= table.m_LuaFile;
	L			= table.L;
	m_szPath	= table.m_szPath;

	if (table.PushTable())
	{
		lua_pushvalue(L, -1); // Copy
		m_RefNum = luaL_ref(L, LUA_REGISTRYINDEX);
	}
	else
		m_RefNum = LUA_NOREF;

	m_bIsValid = (m_RefNum != LUA_NOREF);

	if (m_LuaFile)
		m_LuaFile->AddTable(this);
}


LuaTable& LuaTable::operator=(const LuaTable& table)
{
	if (m_LuaFile && (m_RefNum != LUA_NOREF) && (m_LuaFile->m_CurRef == m_RefNum))
	{
		lua_settop(L, 0);
		m_LuaFile->m_CurRef = LUA_NOREF;
	}

	// Change the Lua File if they differ
	if (m_LuaFile != table.m_LuaFile)
	{
		if (m_LuaFile != NULL)
			m_LuaFile->RemoveTable(this);

		if (m_LuaFile && m_RefNum != LUA_NOREF)
			luaL_unref(L, LUA_REGISTRYINDEX, m_RefNum);

		m_LuaFile = table.m_LuaFile;
	}

	L		 = table.L;
	m_szPath = table.m_szPath;

	if (table.PushTable())
	{
		lua_pushvalue(L, -1); // Copy
		m_RefNum = luaL_ref(L, LUA_REGISTRYINDEX);
	}

	m_bIsValid = (m_RefNum != LUA_NOREF);

	return *this;
}


LuaTable::~LuaTable()
{
	if (L && m_RefNum != LUA_NOREF)
	{
		luaL_unref(L, LUA_REGISTRYINDEX, m_RefNum);
		if (m_LuaFile && m_LuaFile->m_CurRef == m_RefNum)
		{
			lua_settop(L, 0);
			m_LuaFile->m_CurRef = LUA_NOREF;
		}
	}

	if (m_LuaFile)
		m_LuaFile->RemoveTable(this);
}


/***************************************************************************/
// Key Check

bool LuaTable::KeyExists(int key) const
{
	if (!PushValue(key))
		return false;

	lua_pop(L, 1);
	return true;
}


bool LuaTable::KeyExists(const string& key) const
{
	if (!PushValue(key))
		return false;

	lua_pop(L, 1);
	return true;
}


/***************************************************************************/
// Value Types


int LuaTable::GetType(int key) const
{
	if (!PushValue(key))
		return -1;

	const int type = lua_type(L, -1);
	lua_pop(L, 1);
	return type;
}


int LuaTable::GetType(const string& key) const
{
	if (!PushValue(key))
		return -1;

	const int type = lua_type(L, -1);
	lua_pop(L, 1);
	return type;
}


/***************************************************************************/
// Length

int LuaTable::GetLength() const
{
	if (!PushTable())
		return 0;

	return lua_objlen(L, -1);
}


int LuaTable::GetLength(int key) const
{
	if (!PushValue(key))
		return 0;

	const int len = lua_objlen(L, -1);
	lua_pop(L, 1);
	return len;
}


int LuaTable::GetLength(const string& key) const
{
	if (!PushValue(key))
		return 0;

	const int len = lua_objlen(L, -1);
	lua_pop(L, 1);
	return len;
}


/***************************************************************************/
// Key List Functions

bool LuaTable::GetKeys(vector<int>& data) const
{
	if (!PushTable())
		return false;

	const int table = lua_gettop(L);
	for (lua_pushnil(L); lua_next(L, table) != 0; lua_pop(L, 1))
	{
		if (lua_isnumber(L, -2))
		{
			const int value = lua_tointeger(L, -2);
			data.push_back(value);
		}
	}
	std::sort(data.begin(), data.end());
	return true;
}


bool LuaTable::GetKeys(vector<string>& data) const
{
	if (!PushTable())
		return false;

	const int table = lua_gettop(L);
	for (lua_pushnil(L); lua_next(L, table) != 0; lua_pop(L, 1))
	{
		if (lua_isnumber(L, -2))
		{
			const string value = lua_tostring(L, -2);
			data.push_back(value);
		}
	}
	std::sort(data.begin(), data.end());
	return true;
}


/***************************************************************************/
// Map List Functions

bool LuaTable::GetMap(map<int, float>& data) const
{
	if (!PushTable())
		return false;

	const int table = lua_gettop(L);
	for (lua_pushnil(L); lua_next(L, table) != 0; lua_pop(L, 1))
	{
		if (lua_isnumber(L, -2) && lua_isnumber(L, -1))
		{
			const int   key	  = lua_tointeger(L, -2);
			const float value = static_cast<float>(lua_tonumber(L, -1));
			data[key] = value;
		}
	}
	return true;
}


bool LuaTable::GetMap(map<int, string>& data) const
{
	if (!PushTable())
		return false;

	const int table = lua_gettop(L);
	for (lua_pushnil(L); lua_next(L, table) != 0; lua_pop(L, 1))
	{
		if (lua_isnumber(L, -2) && lua_isstring(L, -1))
		{
			if (lua_isstring(L, -1))
			{
				const int    key   = lua_tointeger(L, -2);
				const string value = lua_tostring(L, -1);

				data[key] = value;
			}
			else if (lua_isboolean(L, -1))
			{
				const int    key   = lua_tointeger(L, -2);
				const string value = (lua_toboolean(L, -1) ? "1" : "0");
				data[key] = value;
			}
		}
	}
	return true;
}


bool LuaTable::GetMap(map<string, float>& data) const
{
	if (!PushTable())
		return false;

	const int table = lua_gettop(L);
	for (lua_pushnil(L); lua_next(L, table) != 0; lua_pop(L, 1))
	{
		if (lua_isstring(L, -2) && lua_isnumber(L, -1))
		{
			const string key	= lua_tostring(L, -2);
			const float  value	= static_cast<float>(lua_tonumber(L, -1));

			data[key] = value;
		}
	}
	return true;
}


bool LuaTable::GetMap(map<string, string>& data) const
{
	if (!PushTable())
		return false;

	const int table = lua_gettop(L);
	for (lua_pushnil(L); lua_next(L, table) != 0; lua_pop(L, 1))
	{
		if (lua_isstring(L, -2))
		{
			if (lua_isstring(L, -1))
			{
				const string key = lua_tostring(L, -2);
				const string value = lua_tostring(L, -1);

				data[key] = value;
			}
			else if (lua_isboolean(L, -1))
			{
				const string key	= lua_tostring(L, -2);
				const string value	= (lua_toboolean(L, -1) ? "1" : "0");

				data[key] = value;
			}
		}
	}
	return true;
}


/***************************************************************************/
// Numeric Keys

int LuaTable::GetInt(int key, int def) const
{
	if (!PushValue(key))
		return def;

	if (!lua_isnumber(L, -1))
	{
		lua_pop(L, -1);
		return def;
	}

	const int value = lua_tointeger(L, -1);
	lua_pop(L, 1);
	return value;
}


bool LuaTable::GetBool(int key, bool def) const
{
	if (!PushValue(key))
		return def;

	bool value;
	if (!lua_isboolean(L, -1))
	{
		lua_pop(L, 1);
		return def;
	}
	value = lua_toboolean(L, -1);
	lua_pop(L, 1);
	return value;
}


float LuaTable::GetFloat(int key, float def) const
{
	if (!PushValue(key))
		return def;

	if (!lua_isnumber(L, -1))
	{
		lua_pop(L, 1);
		return def;
	}

	const float value = static_cast<float>(lua_tonumber(L, -1));
	lua_pop(L, 1);
	return value;
}


string LuaTable::GetString(int key, const string& def) const
{
	if (!PushValue(key))
		return def;

	if (!lua_isstring(L, -1))
	{
		lua_pop(L, 1);
		return def;
	}
	const string value = lua_tostring(L, -1);
	lua_pop(L, 1);
	return value;
}


/***************************************************************************/
// String Keys

int LuaTable::GetInt(const string& key, int def) const
{
	if (!PushValue(key))
		return def;

	if (!lua_isnumber(L, -1))
	{
		lua_pop(L, -1);
		return def;
	}

	const int value = lua_tointeger(L, -1);
	lua_pop(L, 1);
	return value;
}


bool LuaTable::GetBool(const string& key, bool def) const
{
	if (!PushValue(key))
		return def;

	bool value;
	if (!lua_isboolean(L, -1))
	{
		lua_pop(L, 1);
		return def;
	}
	value = lua_toboolean(L, -1);
	lua_pop(L, 1);
	return value;
}


float LuaTable::GetFloat(const string& key, float def) const
{
	if (!PushValue(key))
		return def;

	if (!lua_isnumber(L, -1))
	{
		lua_pop(L, 1);
		return def;
	}

	const float value = static_cast<float>(lua_tonumber(L, -1));
	lua_pop(L, 1);
	return value;
}


string LuaTable::GetString(const string& key, const string& def) const
{
	if (!PushValue(key))
		return def;

	if (!lua_isstring(L, -1))
	{
		lua_pop(L, 1);
		return def;
	}
	const string value = lua_tostring(L, -1);
	lua_pop(L, 1);
	return value;
}


/***************************************************************************/
// Sub Table

LuaTable LuaTable::SubTable(int key) const
{
	LuaTable subTable;
	char buf[32];
	sprintf_s(buf, 32, "[%i]", key);
	subTable.m_szPath = m_szPath + buf;

	if (!PushTable())
		return subTable;

	lua_pushnumber(L, key);
	lua_gettable(L, -2);
	if (!lua_istable(L, -1))
	{
		lua_pop(L, 1);
		return subTable;
	}

	subTable.m_LuaFile	= m_LuaFile;
	subTable.L			= L;
	subTable.m_RefNum	= luaL_ref(L, LUA_REGISTRYINDEX);
	subTable.m_bIsValid	= (subTable.m_RefNum != LUA_NOREF);

	m_LuaFile->AddTable(&subTable);

	return subTable;
}


LuaTable LuaTable::SubTable(const string& key) const
{
	LuaTable subTable;
	subTable.m_szPath = m_szPath + "." + key;

	if (!PushTable())
		return subTable;

	lua_pushstring(L, key.c_str());
	lua_gettable(L, -2);
	if (!lua_istable(L, -1))
	{
		lua_pop(L, 1);
		return subTable;
	}

	subTable.m_LuaFile	= m_LuaFile;
	subTable.L			= L;
	subTable.m_RefNum	= luaL_ref(L, LUA_REGISTRYINDEX);
	subTable.m_bIsValid	= (subTable.m_RefNum != LUA_NOREF);

	m_LuaFile->AddTable(&subTable);

	return subTable;
}


LuaTable LuaTable::SubTableExpr(const string& expr) const
{
	if (expr.empty())
		return LuaTable(*this);

	if (!m_bIsValid)
		return LuaTable();

	string::size_type endPos;
	LuaTable nextTable;

	if (expr[0] == '[') // Numeric key
	{
		endPos = expr.find(']');
		if (endPos == string::npos)
			return LuaTable(); // Missing brace

		const char* startPtr = expr.c_str() + 1; // Skip the '['
		char* endPtr;
		const int index = strtol(startPtr, &endPtr, 10);
		if (endPtr == startPtr)
			return LuaTable(); // Invalid index

		endPos++; // Eat the ']'
		nextTable = SubTable(index);
	}
	else // String key
	{
		endPos = expr.find_first_of(".[");
		if (endPos == string::npos)
			return SubTable(expr);
		nextTable = SubTable(expr.substr(0, endPos));
	}

	if (expr[endPos] == '.')
		endPos++; // Eat the dot

	return nextTable.SubTableExpr(expr.substr(endPos));
}


/***************************************************************************/
// Private utility functions

bool LuaTable::PushTable() const
{
	if (!m_bIsValid)
		return false;

	// If table is already at top of the stack, return true
	if (m_RefNum != LUA_NOREF && m_LuaFile->m_CurRef == m_RefNum)
	{
		if (!lua_istable(L, -1))
		{
			m_LuaFile->m_CurRef = LUA_NOREF;
			lua_settop(L, 0);
			return false;
		}
		return true;
	}

	lua_settop(L, 0);

	// Get the table
	lua_rawgeti(L, LUA_REGISTRYINDEX, m_RefNum);

	// Make sure the reference is a table
	if (!lua_istable(L, -1))
	{
		m_bIsValid = false;
		m_LuaFile->m_CurRef = LUA_NOREF;
		lua_settop(L, 0);

		return false;
	}

	m_LuaFile->m_CurRef = m_RefNum;

	return true;
}


bool LuaTable::PushValue(int key) const
{
	if (!PushTable())
		return false;

	lua_pushnumber(L, key);
	lua_gettable(L, -2);
	if (lua_isnoneornil(L, -1))
	{
		lua_pop(L, 1);
		return false;
	}

	return true;
}


bool LuaTable::PushValue(const string& key) const
{
	if (!PushTable())
		return false;

	lua_pushstring(L, key.c_str());
	lua_gettable(L, -2);
	if (lua_isnoneornil(L, -1))
	{
		lua_pop(L, 1);
		return false;
	}

	return true;
}